using System;
using System.Collections.Generic;

namespace PropertyExpression.ReactiveRelation
{
    public static class CachedFuncRegistrator
    {
        public static Func<FuncCache, Func<T, TResult>> 
            CacheFunc<T, TResult>(Func<T, TResult> func)
        {
            var funcKey = GetFuncKey();
            return funcCache => arg =>
                   CacheFuncWithArgument<CachedFuncArgument<T>, TResult>(
                       funcKey,
                       innerArg => func(innerArg.Arg)
                       )(funcCache)(new CachedFuncArgument<T>(arg));
        }

        public static Func<FuncCache, Func<T1, T2, TResult>>
            CacheFunc<T1, T2, TResult>(Func<T1, T2, TResult> func)
        {
            var funcKey = GetFuncKey();
            return funcCache => (arg1, arg2) =>
                   CacheFuncWithArgument<CachedFuncArgument<T1, T2>, TResult>(
                       funcKey,
                       innerArg => func(innerArg.Arg1, innerArg.Arg2)
                       )(funcCache)(new CachedFuncArgument<T1, T2>(arg1, arg2));
        }

        public static Func<FuncCache, Func<T1, T2, T3, TResult>>
            CacheFunc<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> func)
        {
            var funcKey = GetFuncKey();
            return funcCache => (arg1, arg2, arg3) =>
                   CacheFuncWithArgument<CachedFuncArgument<T1, T2, T3>, TResult>(
                       funcKey,
                       innerArg => func(innerArg.Arg1, innerArg.Arg2, innerArg.Arg3)
                       )(funcCache)(new CachedFuncArgument<T1, T2, T3>(arg1, arg2, arg3));
        }

        public static Func<FuncCache, Func<T1, T2, T3, T4, TResult>>
            CacheFunc<T1, T2, T3, T4, TResult>(Func<T1, T2, T3, T4, TResult> func)
        {
            var funcKey = GetFuncKey();
            return funcCache => (arg1, arg2, arg3, arg4) =>
                   CacheFuncWithArgument<CachedFuncArgument<T1, T2, T3, T4>, TResult>(
                       funcKey,
                       innerArg => func(innerArg.Arg1, innerArg.Arg2, innerArg.Arg3, innerArg.Arg4)
                       )(funcCache)(new CachedFuncArgument<T1, T2, T3, T4>(arg1, arg2, arg3, arg4));
        }

        public static Func<FuncCache, Func<T, TResult>> 
            CacheFuncWithArgument<T, TResult>(
            Func<T, TResult> func) where T : ICachedFuncArgument
        {
            var funcKey = GetFuncKey();
            return CacheFuncWithArgument(funcKey, func);
        }

        public static Func<FuncCache, TCalcContext, Func<T, TResult>> 
            CacheFunc<TCalcContext, T, TResult>(
            Func<TCalcContext, Func<T, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext) => arg =>
                   CacheFuncWithArgument<TCalcContext, CachedFuncArgument<T>, TResult>(
                       funcKey,
                       innerCalcContext =>
                       innerArg => func(innerCalcContext)(innerArg.Arg)
                       )(funcCache, calcContext)(new CachedFuncArgument<T>(arg));
        }

        public static Func<FuncCache, TCalcContext, Func<T1, T2, TResult>>
            CacheFunc<TCalcContext, T1, T2, TResult>(
            Func<TCalcContext, Func<T1, T2, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext) => (arg1, arg2) =>
                   CacheFuncWithArgument<TCalcContext, CachedFuncArgument<T1, T2>, TResult>(
                       funcKey,
                       innerCalcContext =>
                       innerArg => func(innerCalcContext)(innerArg.Arg1, innerArg.Arg2)
                       )(funcCache, calcContext)(new CachedFuncArgument<T1, T2>(arg1, arg2));
        }

        public static Func<FuncCache, TCalcContext, Func<T1, T2, T3, TResult>>
            CacheFunc<TCalcContext, T1, T2, T3, TResult>(
            Func<TCalcContext, Func<T1, T2, T3, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext) => (arg1, arg2, arg3) =>
                   CacheFuncWithArgument<TCalcContext, CachedFuncArgument<T1, T2, T3>, TResult>(
                       funcKey,
                       innerCalcContext =>
                       innerArg => func(innerCalcContext)(innerArg.Arg1, innerArg.Arg2, innerArg.Arg3)
                       )(funcCache, calcContext)(new CachedFuncArgument<T1, T2, T3>(arg1, arg2, arg3));
        }

        public static Func<FuncCache, TCalcContext, Func<T1, T2, T3, T4, TResult>>
            CacheFunc<TCalcContext, T1, T2, T3, T4, TResult>(
            Func<TCalcContext, Func<T1, T2, T3, T4, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext) => (arg1, arg2, arg3, arg4) =>
                   CacheFuncWithArgument<TCalcContext, CachedFuncArgument<T1, T2, T3, T4>, TResult>(
                       funcKey,
                       innerCalcContext =>
                       innerArg => func(innerCalcContext)(
                           innerArg.Arg1, innerArg.Arg2, innerArg.Arg3, innerArg.Arg4)
                       )(funcCache, calcContext)(new CachedFuncArgument<T1, T2, T3, T4>(
                           arg1, arg2, arg3, arg4));
        }

        public static Func<FuncCache, TCalcContext, Func<T, TResult>>
            CacheFuncWithArgument<TCalcContext, T, TResult>(
            Func<TCalcContext, Func<T, TResult>> func)
            where T : ICachedFuncArgument
        {
            var funcKey = GetFuncKey();
            return CacheFuncWithArgument(funcKey, func);
        }

        public static Func<FuncCache, TEntity, Func<TResult>>
            CacheEntityFunc<TEntity, TResult>(
            Func<TEntity, Func<TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, entity) => () =>
                   CacheEntityFuncWithArgument<TEntity, CachedFuncArgument, TResult>(
                       funcKey,
                       innerEntity => innerArg => func(innerEntity)()
                       )(funcCache, entity)(new CachedFuncArgument());
        }

        public static Func<FuncCache, TEntity, Func<T, TResult>> 
            CacheEntityFunc<TEntity, T, TResult>(
            Func<TEntity, Func<T, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, entity) => arg =>
                   CacheEntityFuncWithArgument<TEntity, CachedFuncArgument<T>, TResult>(
                       funcKey,
                       innerEntity => innerArg => func(innerEntity)(innerArg.Arg)
                       )(funcCache, entity)(new CachedFuncArgument<T>(arg));
        }

        public static Func<FuncCache, TEntity, Func<T1, T2, TResult>>
            CacheEntityFunc<TEntity, T1, T2, TResult>(
            Func<TEntity, Func<T1, T2, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, entity) => (arg1, arg2) =>
                   CacheEntityFuncWithArgument<TEntity, CachedFuncArgument<T1, T2>, TResult>(
                       funcKey,
                       innerEntity => innerArg =>
                           func(innerEntity)(innerArg.Arg1, innerArg.Arg2)
                       )(funcCache, entity)(new CachedFuncArgument<T1, T2>(arg1, arg2));
        }

        public static Func<FuncCache, TEntity, Func<T1, T2, T3, TResult>>
            CacheEntityFunc<TEntity, T1, T2, T3, TResult>(
            Func<TEntity, Func<T1, T2, T3, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, entity) => (arg1, arg2, arg3) =>
                   CacheEntityFuncWithArgument<TEntity, CachedFuncArgument<T1, T2, T3>, TResult>(
                       funcKey,
                       innerEntity => innerArg =>
                           func(innerEntity)(innerArg.Arg1, innerArg.Arg2, innerArg.Arg3)
                       )(funcCache, entity)(new CachedFuncArgument<T1, T2, T3>(arg1, arg2, arg3));
        }

        public static Func<FuncCache, TEntity, Func<T1, T2, T3, T4, TResult>>
            CacheEntityFunc<TEntity, T1, T2, T3, T4, TResult>(
            Func<TEntity, Func<T1, T2, T3, T4, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, entity) => (arg1, arg2, arg3, arg4) =>
                   CacheEntityFuncWithArgument<TEntity, CachedFuncArgument<T1, T2, T3, T4>, TResult>(
                       funcKey,
                       innerEntity => innerArg =>
                           func(innerEntity)(innerArg.Arg1, innerArg.Arg2, innerArg.Arg3, innerArg.Arg4)
                       )(funcCache, entity)(new CachedFuncArgument<T1, T2, T3, T4>(arg1, arg2, arg3, arg4));
        }

        public static Func<FuncCache, TEntity, Func<T, TResult>>
            CacheEntityFuncWithArgument<TEntity, T, TResult>(
            Func<TEntity, Func<T, TResult>> func)
            where T : ICachedFuncArgument
        {
            var funcKey = GetFuncKey();
            return CacheEntityFuncWithArgument(funcKey, func);
        }

        public static Func<FuncCache, TCalcContext, TEntity, Func<T, TResult>>
            CacheEntityFunc<TCalcContext, TEntity, T, TResult>(
            Func<TCalcContext, TEntity, Func<T, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext, entity) => arg =>
                   CacheEntityFuncWithArgument<TCalcContext, TEntity, CachedFuncArgument<T>, TResult>(
                       funcKey,
                       (innerCalcContext, innerEntity) =>
                       innerArg => func(innerCalcContext, innerEntity)(innerArg.Arg)
                       )(funcCache, calcContext, entity)(new CachedFuncArgument<T>(arg));
        }

        public static Func<FuncCache, TCalcContext, TEntity, Func<T1, T2, TResult>>
            CacheEntityFunc<TCalcContext, TEntity, T1, T2, TResult>(
            Func<TCalcContext, TEntity, Func<T1, T2, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext, entity) => (arg1, arg2) =>
                   CacheEntityFuncWithArgument<TCalcContext, TEntity, CachedFuncArgument<T1, T2>, TResult>(
                       funcKey,
                       (innerCalcContext, innerEntity) =>
                       innerArg => func(innerCalcContext, innerEntity)(
                           innerArg.Arg1, innerArg.Arg2)
                       )(funcCache, calcContext, entity)(new CachedFuncArgument<T1, T2>(arg1, arg2));
        }

        public static Func<FuncCache, TCalcContext, TEntity, Func<T1, T2, T3, TResult>>
            CacheEntityFunc<TCalcContext, TEntity, T1, T2, T3, TResult>(
            Func<TCalcContext, TEntity, Func<T1, T2, T3, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext, entity) => (arg1, arg2, arg3) =>
                   CacheEntityFuncWithArgument<TCalcContext, TEntity, CachedFuncArgument<T1, T2, T3>, TResult>(
                       funcKey,
                       (innerCalcContext, innerEntity) =>
                       innerArg => func(innerCalcContext, innerEntity)(
                           innerArg.Arg1, innerArg.Arg2, innerArg.Arg3)
                       )(funcCache, calcContext, entity)(new CachedFuncArgument<T1, T2, T3>(arg1, arg2, arg3));
        }

        public static Func<FuncCache, TCalcContext, TEntity, Func<T1, T2, T3, T4, TResult>>
            CacheEntityFunc<TCalcContext, TEntity, T1, T2, T3, T4, TResult>(
            Func<TCalcContext, TEntity, Func<T1, T2, T3, T4, TResult>> func)
        {
            var funcKey = GetFuncKey();
            return (funcCache, calcContext, entity) => (arg1, arg2, arg3, arg4) =>
                   CacheEntityFuncWithArgument<TCalcContext, TEntity, CachedFuncArgument<T1, T2, T3, T4>, TResult>(
                       funcKey,
                       (innerCalcContext, innerEntity) =>
                       innerArg => func(innerCalcContext, innerEntity)(
                           innerArg.Arg1, innerArg.Arg2, innerArg.Arg3, innerArg.Arg4)
                       )(funcCache, calcContext, entity)(new CachedFuncArgument<T1, T2, T3, T4>(arg1, arg2, arg3, arg4));
        }

        public static Func<FuncCache, TCalcContext, TEntity, Func<T, TResult>>
            CacheEntityFuncWithArgument<TCalcContext, TEntity, T, TResult>(
            Func<TCalcContext, TEntity, Func<T, TResult>> func)
            where T : ICachedFuncArgument
        {
            var funcKey = GetFuncKey();
            return CacheEntityFuncWithArgument(funcKey, func);
        }

        private static Func<FuncCache, Func<T, TResult>> 
            CacheFuncWithArgument<T, TResult>(
            object funcKey,
            Func<T, TResult> func)
            where T : ICachedFuncArgument
        {
            return funcCache => cachedFuncArgument =>
                   funcCache.GetValue(
                       funcKey,
                       cachedFuncArgument,
                       func
                       );
        }

        private static Func<FuncCache, TCalcContext, Func<T, TResult>>
            CacheFuncWithArgument<TCalcContext, T, TResult>(
            object funcKey,
            Func<TCalcContext, Func<T, TResult>> func)
            where T : ICachedFuncArgument
        {
            return (funcCache, calcContext) => cachedFuncArgument =>
                   funcCache.GetValue(
                       funcKey,
                       cachedFuncArgument,
                       arg => func(calcContext)(arg)
                       );
        }

        private static Func<FuncCache, TEntity, Func<T, TResult>> 
            CacheEntityFuncWithArgument<TEntity, T, TResult>(
            object funcKey,
            Func<TEntity, Func<T, TResult>> func)
            where T : ICachedFuncArgument
        {
            return (funcCache, entity) => cachedFuncArgument =>
                   funcCache.GetValue(
                       funcKey,
                       cachedFuncArgument,
                       arg => func(entity)(arg)
                       );
        }

        private static Func<FuncCache, TCalcContext, TEntity, Func<T, TResult>>
            CacheEntityFuncWithArgument<TCalcContext, TEntity, T, TResult>(
            object funcKey,
            Func<TCalcContext, TEntity, Func<T, TResult>> func)
            where T : ICachedFuncArgument
        {
            return (funcCache, calcContext, entity) => cachedFuncArgument =>
                   funcCache.GetValue(
                       funcKey,
                       cachedFuncArgument,
                       arg => func(calcContext,
                                   entity)(arg)
                       );
        }

        private static object GetFuncKey()
        {
            return new object();
        }

        private class CachedFuncArgument : ICachedFuncArgument
        {
            public IEnumerable<object> Arguments
            {
                get
                {
                    return new object[] {};
                }
            }
        }

        private class CachedFuncArgument<T> : ICachedFuncArgument
        {
            private readonly T arg;

            public CachedFuncArgument(T arg)
            {
                this.arg = arg;
            }

            public T Arg
            {
                get { return arg; }
            }

            public IEnumerable<object> Arguments
            {
                get
                {
                    yield return Arg;
                }
            }
        }

        private class CachedFuncArgument<T1, T2> : ICachedFuncArgument
        {
            private readonly T1 arg1;
            private readonly T2 arg2;

            public CachedFuncArgument(T1 arg1, T2 arg2)
            {
                this.arg1 = arg1;
                this.arg2 = arg2;
            }

            public T1 Arg1
            {
                get { return arg1; }
            }

            public T2 Arg2
            {
                get { return arg2; }
            }

            public IEnumerable<object> Arguments
            {
                get
                {
                    yield return Arg1;
                    yield return Arg2;
                }
            }
        }

        private class CachedFuncArgument<T1, T2, T3> : ICachedFuncArgument
        {
            private readonly T1 arg1;
            private readonly T2 arg2;
            private readonly T3 arg3;

            public CachedFuncArgument(T1 arg1, T2 arg2, T3 arg3)
            {
                this.arg1 = arg1;
                this.arg2 = arg2;
                this.arg3 = arg3;
            }

            public T1 Arg1
            {
                get { return arg1; }
            }

            public T2 Arg2
            {
                get { return arg2; }
            }

            public T3 Arg3
            {
                get { return arg3; }
            }

            public IEnumerable<object> Arguments
            {
                get
                {
                    yield return Arg1;
                    yield return Arg2;
                    yield return Arg3;
                }
            }
        }

        private class CachedFuncArgument<T1, T2, T3, T4> : ICachedFuncArgument
        {
            private readonly T1 arg1;
            private readonly T2 arg2;
            private readonly T3 arg3;
            private readonly T4 arg4;

            public CachedFuncArgument(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
            {
                this.arg1 = arg1;
                this.arg2 = arg2;
                this.arg3 = arg3;
                this.arg4 = arg4;
            }

            public T1 Arg1
            {
                get { return arg1; }
            }

            public T2 Arg2
            {
                get { return arg2; }
            }

            public T3 Arg3
            {
                get { return arg3; }
            }

            public T4 Arg4
            {
                get { return arg4; }
            }

            public IEnumerable<object> Arguments
            {
                get
                {
                    yield return Arg1;
                    yield return Arg2;
                    yield return Arg3;
                    yield return Arg4;
                }
            }
        }
    }
}